#include "ThreadSimpleSyncDemoOfC.h"
#include <pthread.h>
#include <unistd.h>
#include <iostream>

using namespace std;

#define THREAD_COUNT_MAX 250
// 全局变量：用来累计计数，两个线程会访问这个变量并累加它，一个线程累加50个数，实现从0~100的累计
int thread_common_number = 0;

#ifdef __APPLE__
string to_string(pthread_t t)
{
    return to_string(t->__sig);
}
#endif

void* cumulativeCountA(void*) {
    for(int index = 0; index < THREAD_COUNT_MAX; ++index) {
        int count = thread_common_number;
        count++;
        usleep(10);
        thread_common_number = count;
        cout << "thread id: " + to_string(pthread_self())  + ", number:" + to_string(thread_common_number) + '\n';
    }
    return nullptr;
}

void* cumulativeCountB(void*) {
    for(int index = 0; index < THREAD_COUNT_MAX; ++index) {
        int count = thread_common_number;
        count++;
        thread_common_number = count;
        cout << "thread id: " + to_string(pthread_self())  + ", number:" + to_string(thread_common_number) + '\n';
        usleep(5);
    }
    return nullptr;
}

// 创建一把互斥锁
// 全局变量, 多个线程共享
pthread_mutex_t mutex;
void* syncCumulativeCountA(void*) {
    for(int index = 0; index < THREAD_COUNT_MAX; ++index) {
        // 如果线程 A 加锁成功, 不阻塞
        // 如果 B 加锁成功, 线程 A 阻塞
        pthread_mutex_lock(&mutex);
        int count = thread_common_number;
        count++;
        usleep(10);
        thread_common_number = count;
        // 解锁
        pthread_mutex_unlock(&mutex);
        cout << "thread id: " + to_string(pthread_self())  + ", number:" + to_string(thread_common_number) + '\n';
    }
    return nullptr;
}

void* syncCumulativeCountB(void*) {
    // 不把解锁放在循环外是因为那样会让多线程变成单线程，意味着一旦某个线程进入锁后要等整个循环执行结束解锁才能获得执行。
    // 但是把加锁放在线程内，会导致每次线程执行都需要枷锁。实际上会导致上下文切换频繁，这样得多线程效率甚至不如单线程。
    for(int index = 0; index < THREAD_COUNT_MAX; ++index) {
        // A 加锁成功, B 线程访问这把锁的时候是锁定的
        // 线程 B 先阻塞, A 线程解锁之后阻塞解除
        // 线程 B 加锁成功了
        pthread_mutex_lock(&mutex);
        int count = thread_common_number;
        count++;
        thread_common_number = count;
        // 解锁
        pthread_mutex_unlock(&mutex);
        cout << "thread id: " + to_string(pthread_self())  + ", number:" + to_string(thread_common_number) + '\n';
        usleep(5);
    }
    return nullptr;
}

void threadSimpleSyncDemoOfC()
{
    thread_common_number = 0;

    // 线程同步概念
    // 在多个线程需要对同一内存中的共享资源进行访问时，在同一时刻只允许一个线程进行访问操作，其他线程需要等待访问结束后才能随机竞争访问权限。像这样线程对内存的这种访问方式就称之为线程同步。
    // 通过对概念的介绍，我们可以了解到所谓的同步并不是多个线程同时对内存进行访问，而是按照先后顺序依次进行的。同步串行（阻塞），异步并行。

    // 1. 为什么要同步
    // 在研究线程同步之前，先来看一个两个线程交替数数（每个线程数50个数，交替数到100）的例子：

    /*
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, cumulativeCountA, nullptr);
    pthread_create(&tid2, nullptr, cumulativeCountB, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    */

    // 上面的结果，每个线程会执行 250 次，但执行的最后结果 thread_common_number 并不总是等于 500，其原因就是因为线程对 thread_common_number 访问并没有进行同步处理，导致了脏读脏写。
    // 两个线程在数数的时候需要分时复用 CPU 时间片，并且测试程序中调用了 sleep() 导致线程的 CPU 时间片没用完就被迫挂起了，
    // 这样就能让 CPU 的上下文切换（保存当前状态, 下一次继续运行的时候需要加载保存的状态）更加频繁，更容易再现数据混乱的这个现象。
    // sleep() 睡眠秒级  usleep() 睡眠微秒级 nanosleep 睡眠纳秒级
    /*
        ┌───────────┐       ┌───────┐       ┌───────┐       ┌───────┐                                ┌─────────┐
        │    CPU    │       │  L1   │       │  L2   │       │  L3   │           ┌────────┐    I/O    │  Disk   │
        │ registers │ <---> │ Cache │ <---> │ Cache │ <---> │ Cache │ <-------> │ Memory │ <-------> │ Storage │
        │           │       │       │       │       │       │       │           └────────┘           │         │
        └───────────┘       └───────┘       └───────┘       └───────┘                                └─────────┘
          CPU 寄存器         一级缓存         二级缓存         三级缓存             物理内存              磁盘存储
    */
    // CPU对应寄存器、一级缓存、二级缓存、三级缓存是独占的，用于存储处理的数据和线程的状态信息，数据被 CPU 处理完成需要再次被写入到物理内存中，物理内存数据也可以通过文件 IO 操作写入到磁盘中。
    // 在测试程序中两个线程共用全局变量 thread_common_number 当线程变成运行态之后开始数数，从物理内存加载数据，让后将数据放到 CPU 进行运算，最后将结果更新到物理内存中。
    // 如果数数的两个线程都可以顺利完成这个流程，那么得到的结果肯定是正确的。但是实际上如下：
    //  * 线程 A 执行这个过程期间就失去了 CPU 时间片，线程 A 被挂起了最新的数据没能更新到物理内存。
    //  * 线程 B 变成运行态之后从物理内存读数据，很显然它没有拿到最新数据，只能基于旧的数据往后数，然后失去 CPU 时间片挂起。
    //  * 线程 A 得到 CPU 时间片变成运行态，第一件事儿就是将上次没更新到内存的数据更新到内存，但是这样会导致线程 B 已经更新到内存的数据被覆盖，活儿白干了，最终导致有些数据会被重复数很多次。


    // 2. 同步方式
    // 对于多个线程访问共享资源出现数据混乱的问题，需要进行线程同步。常用的线程同步方式有四种：互斥锁、读写锁、条件变量、信号量。
    // 
    // 所谓的共享资源就是多个线程共同访问的变量，这些变量通常为全局数据区变量或者堆区变量，这些变量对应的共享资源也被称之为临界资源。
    // 找到临界资源之后，再找和临界资源相关的上下文代码，这样就得到了一个代码块，这个代码块可以称之为临界区。
    // 确定好临界区（临界区越小越好）之后，就可以进行线程同步了，线程同步的大致处理思路是这样的：
    //  * 在临界区代码的上边，添加加锁函数，对临界区加锁。
    //    哪个线程调用这句代码，就会把这把锁锁上，其他线程就只能阻塞在锁上了。
    //  * 在临界区代码的下边，添加解锁函数，对临界区解锁。
    //    出临界区的线程会将锁定的那把锁打开，其他抢到锁的线程就可以进入到临界区了。
    //  * 通过锁机制能保证临界区代码最多只能同时有一个线程访问，这样并行访问就变为串行访问了。


    // 2.1 互斥锁
    // 互斥锁是线程同步最常用的一种方式，通过互斥锁可以锁定一个代码块, 被锁定的这个代码块, 所有的线程只能顺序执行(不能并行处理)，这样多线程访问共享资源数据混乱的问题就可以被解决了，
    // 需要付出的代价就是执行效率的降低，因为默认临界区多个线程是可以并行处理的，现在只能串行处理。
    // 
    // 在 Linux 中互斥锁的类型为 pthread_mutex_t，创建一个这种类型的变量就得到了一把互斥锁：
    // pthread_mutex_t mutex;
    
    // 在创建的锁对象中保存了当前这把锁的状态信息：锁定 或 打开。
    //  * 如果是锁定状态还记录了给这把锁加锁的线程信息（线程 ID）。
    //  * 一个互斥锁变量只能被一个线程锁定，被锁定之后其他线程再对互斥锁变量加锁就会被阻塞，直到这把互斥锁被解锁，被阻塞的线程才能被解除阻塞。
    //  * 一般情况下，每一个共享资源对应一个把互斥锁，锁的个数和线程的个数无关。

    // Linux 提供的互斥锁操作函数如下，如果函数调用成功会返回 0，调用失败会返回相应的错误号：
 
    // 初始化互斥锁：
    //  - restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
    // 参数:
    // mutex: 互斥锁变量的地址
    // attr: 互斥锁的属性, 一般使用默认属性即可, 这个参数指定为 NULL
    // int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    
    // 释放互斥锁资源：
    // int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    // 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中，这个函数被调用, 首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:
    //  * 没有被锁定, 是打开的, 这个线程可以加锁成功, 这个锁中会记录是哪个线程加锁成功了
    //  * 如果被锁定了, 其他线程加锁就失败了, 这些线程都会阻塞在这把锁上
    //  * 当这把锁被解开之后, 这些阻塞在锁上的线程就解除阻塞了，并且这些线程是通过竞争的方式对这把锁加锁，没抢到锁的线程继续阻塞
    // int pthread_mutex_lock(pthread_mutex_t *mutex);

    // 尝试加锁，调用这个函数对互斥锁变量加锁还是有两种情况:
    //  * 如果这把锁没有被锁定是打开的，线程加锁成功
    //  * 如果锁变量被锁住了，调用这个函数加锁的线程，不会被阻塞，加锁失败直接返回错误号
    // int pthread_mutex_trylock(pthread_mutex_t *mutex);

    // 对互斥锁解锁
    //  * 不是所有的线程都可以对互斥锁解锁，哪个线程加的锁, 哪个线程才能解锁成功。
    // int pthread_mutex_unlock(pthread_mutex_t *mutex);

    // 具体互斥锁使用
    // 初始化互斥锁：
    //  * 在某些情况下即便没有初始化互斥锁，其依然正确输出了结果，这并不意味着可以省去初始化行为，这样做在某些情况下并不安全。
    //  * 另外某些时候初始化互斥锁是为了指定锁属性。
    pthread_mutex_init(&mutex, NULL);

    pthread_t syncTid1, syncTid2;
    pthread_create(&syncTid1, nullptr, syncCumulativeCountA, nullptr);
    pthread_create(&syncTid2, nullptr, syncCumulativeCountB, nullptr);

    pthread_join(syncTid1, nullptr);
    pthread_join(syncTid2, nullptr);

    // 销毁互斥锁：线程销毁之后, 再去释放互斥锁
    //  * 如果初始化了互斥锁一定要销毁，因为它会造成内存泄漏。
    //  * 如果没有初始化互斥锁，那么销毁互斥锁时一个未定义得行为，可能出现错误。
    pthread_mutex_destroy(&mutex);

    // 死锁
    // 当多个线程访问共享资源, 需要加锁, 如果锁使用不当, 就会造成死锁这种现象。如果线程死锁造成的后果是：
    //  * 所有的线程都被阻塞，并且线程的阻塞是无法解开的（因为可以解锁的线程也被阻塞了）。
    // 造成死锁的场景有如下几种：

    //  * 加锁之后忘记解锁
    /*
    pthread_t dieTid1, dieTid2;
    pthread_create(&dieTid1, nullptr, [](void*) -> void* {
        // 场景一：
        // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
        // 其余的线程也被阻塞
    	pthread_mutex_lock(&mutex);
        cout << "这里会死锁" << endl;

        // 忘记解锁
    }, nullptr);

    pthread_create(&dieTid2, nullptr, [](void*) -> void* {
        // 场景二：
        // 当前线程A加锁成功
        // 其余的线程被阻塞
    	pthread_mutex_lock(&mutex);
        cout << "这里因为某个分支判断，退出钱没有解锁也会死锁" << endl;
        if(true)
        {
            // 函数退出, 没有解锁（解锁函数无法被执行了）
            return ;
        }
        pthread_mutex_lock(&mutex);
    }, nullptr);
    pthread_join(dieTid1, nullptr);
    pthread_join(dieTid2, nullptr);
    */

    //  * 重复加锁, 造成死锁
    /*
    pthread_t dieTid1, dieTid2;
    pthread_create(&dieTid1, nullptr, [](void*) -> void* {
        // 场景一：
        // 当前线程 A 加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        // 锁被锁住了, A 线程阻塞
        pthread_mutex_lock(&mutex);
        
        cout << "这里会死锁" << endl;

        pthread_mutex_unlock(&mutex);
    }, nullptr);

    // 场景二：
    // 隐藏的比较深的情况
    void funcA()
    {
        for(int i=0; i<6; ++i)
        {
            // 当前线程A加锁成功
            // 其余的线程阻塞
            pthread_mutex_lock(&mutex);
            
            cout << "这里会死锁" << endl;

            pthread_mutex_unlock(&mutex);
        }
    }

    void* funcB(void *)
    {
        for(int i=0; i<6; ++i)
        {
            // 当前线程A加锁成功
            // 其余的线程阻塞
            pthread_mutex_lock(&mutex);
            funcA();		// 重复加锁
            // 别的事
            pthread_mutex_unlock(&mutex);
        }
    }
    pthread_create(&dieTid1, nullptr, funcB, nullptr);
    */

    //  * 在程序中有多个共享资源, 因此有很多把锁，随意加锁，导致相互被阻塞
    // 场景描述:
    // 1. 有两个共享资源:X、Y; X 对应锁 A, Y 对应锁 B
    //     - 线程 A 访问资源 X, 加锁 A, 准备继续访问 Y。
    //     - 线程 B 访问资源 Y, 加锁 B, 准备继续访问 X。
    // 2. 线程 A 要访问资源 Y, 线程 B 要访问资源 X，因为资源 X 和 Y 已经被对应的锁锁住了，因此这个两个线程被阻塞
    //     - 线程 A 被锁 B 阻塞了, 无法打开 A 锁
    //     - 线程 B 被锁 A 阻塞了, 无法打开 B 锁

    // 在使用多线程编程的时候，如何避免死锁呢？
    //  * 避免多次锁定, 多检查
    //  * 对共享资源访问完毕之后, 一定要解锁，或者在加锁的使用 trylock
    //  * 如果程序中有多把锁, 可以控制对锁的访问顺序(顺序访问共享资源，但在有些情况下是做不到的)，另外也可以在对其他互斥锁做加锁操作之前，先释放当前线程拥有的互斥锁。
    //  * 项目程序中可以引入一些专门用于死锁检测的模块


    // 2.2 读写锁
    // 读写锁是互斥锁的升级版, 在做读操作的时候可以提高程序的执行效率，如果所有的线程都是做读操作, 那么读是并行的，但是使用互斥锁，读操作也是串行的。
    // 读写锁是一把锁，锁的类型为 pthread_rwlock_t，有了类型之后就可以创建一把互斥锁了：
    // pthread_rwlock_t rwlock;

    // 之所以称其为读写锁，是因为这把锁既可以锁定读操作，也可以锁定写操作。为了方便理解，可以大致认为在这把锁中记录了这些信息：
    //  * 锁的状态: 锁定/打开
    //  * 锁定的是什么操作: 读操作/写操作，使用读写锁锁定了读操作，需要先解锁才能去锁定写操作，反之亦然。
    //  * 哪个线程将这把锁锁上了
    // 
    // 读写锁的使用方式也互斥锁的使用方式是完全相同的：找共享资源, 确定临界区，在临界区的开始位置加锁（读锁/写锁），临界区的结束位置解锁

    // 因为通过一把读写锁可以锁定读或者写操作，下面介绍一下关于读写锁的特点：
    //  * 使用读写锁的读锁锁定了临界区，线程对临界区的访问是并行的，读锁是共享的。
    //  * 使用读写锁的写锁锁定了临界区，线程对临界区的访问是串行的，写锁是独占的。
    //  * 使用读写锁分别对两个临界区加了读锁和写锁，两个线程要同时访问者两个临界区，访问写锁临界区的线程继续运行，访问读锁临界区的线程阻塞，因为写锁比读锁的优先级高。
    //
    // 如果说程序中所有的线程都对共享资源做写操作，使用读写锁没有优势，和互斥锁是一样的，如果说程序中所有的线程都对共享资源有写也有读操作，并且对共享资源读的操作越多，读写锁更有优势。

    // Linux 提供的读写锁操作函数原型如下，如果函数调用成功返回 0，失败返回对应的错误号：
    
    // pthread_rwlock_t rwlock;
    // 初始化读写锁, 参数:
    //  * rwlock: 读写锁的地址，传出参数
    //  * attr: 读写锁属性，一般使用默认属性，指定为NULL
    // int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    // 释放读写锁占用的系统资源
    // int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

    // 在程序中对 '读写锁' 加 '读锁', 锁定的是 '读操作', 调用这个函数:
    //  * 如果 '读写锁' 是打开的，那么加锁成功；
    //  * 如果 '读写锁' 已经锁定了 '读操作'，调用这个函数依然可以加锁成功，因为读锁是共享的；
    //  * 如果 '读写锁' 已经锁定了 '写操作'，调用这个函数的线程会被阻塞。
    // int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

    // 这个函数可以有效的避免死锁。如果 '加读锁' 失败, 不会阻塞当前线程, 直接返回错误号，调用这个函数：
    //  * 如果 '读写锁' 是打开的，那么加锁成功；
    //  * 如果 '读写锁' 已经锁定了 '读操作'，调用这个函数依然可以加锁成功，因为读锁是共享的；
    //  * 如果 '读写锁' 已经锁定了 '写操作'，调用这个函数加锁失败，对应的线程不会被阻塞，可以在程序中对函数返回值进行判断，添加加锁失败之后的处理动作。
    // int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

    // 在程序中对读写锁加写锁, 锁定的是写操作, 调用这个函数:
    //  * 如果 '读写锁' 是打开的，那么加锁成功；
    //  * 如果 '读写锁' 已经锁定了 '读操作' 或者锁定了 '写操作'，调用这个函数的线程会被阻塞。
    // int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

    // 这个函数可以有效的避免死锁。如果 '加写锁' 失败, 不会阻塞当前线程, 直接返回错误号，调用这个函数：
    //  * 如果 '读写锁' 是打开的，那么加锁成功；
    //  * 如果 '读写锁' 已经锁定了 '读操作' 或者锁定了 '写操作'，调用这个函数加锁失败，但是线程不会阻塞，可以在程序中对函数返回值进行判断，添加加锁失败之后的处理动作。
    // int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

    // 解锁, 不管锁定了读还是写都可用解锁
    // int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);


    // 读写锁使用
    // 题目要求：8个线程操作同一个全局变量，2个线程不定时写同一全局资源，3个线程不定时读同一全局资源。
    /*
    // 定义读写锁
    pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock, nullptr);

    // 写的线程的处理函数
    void* writeCumulativeCount(void*) {
        while(true) 
        {
            pthread_rwlock_wrlock(&rwlock);
            int count = thread_common_number;
            count ++;
            thread_common_number = count;
            cout << "写线程操作，number：" + to_string(thread_common_number) + ";tid::" + to_string(pthread_self()) + '\n';
            pthread_rwlock_unlock(&rwlock);
            // 添加一个随机睡眠
            usleep(rand() % 100);
        }
        return nullptr;
    }

    // 读线程的处理函数
    // 多个线程可以如果处理动作相同, 可以使用相同的处理函数
    // 每个线程中的栈资源是独享
    void* readCumulativeCount(void*) {
        while(true)
        {
            pthread_rwlock_rdlock(&rwlock);
            printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());
            pthread_rwlock_unlock(&rwlock);
            usleep(rand() % 100);
        }
        return NULL;
    }

    pthread_t writeTid1, writeTid2, readTid1, readTid2, readTid3;
    pthread_create(&writeTid1, nullptr, writeCumulativeCount, nullptr);
    pthread_create(&writeTid2, nullptr, writeCumulativeCount, nullptr);
    
    pthread_create(&readTid1, nullptr, readCumulativeCount, nullptr);
    pthread_create(&readTid2, nullptr, readCumulativeCount, nullptr);
    pthread_create(&readTid3, nullptr, readCumulativeCount, nullptr);

    pthread_rwlock_destroy(&rwlock);
    */
   

    // 2.3 条件变量
    // 严格意义上来说，条件变量的主要作用不是处理线程同步, 而是进行线程的阻塞。
    // 如果在多线程程序中只使用条件变量无法实现线程的同步, 必须要配合互斥锁来使用。
    // 虽然条件变量和互斥锁都能阻塞线程，但是二者的效果是不一样的，二者的区别如下：
    //  * 假设有 A-Z 26 个线程，这 26 个线程共同访问同一把互斥锁，如果线程 A 加锁成功，那么其余 B-Z 线程访问互斥锁都阻塞，所有的线程只能顺序访问临界区。
    //  * 条件变量只有在满足指定条件下才会阻塞线程，如果条件不满足，多个线程可以同时进入临界区，同时读写临界资源，这种情况下还是会出现共享资源中数据的混乱。

    // 一般情况下条件变量用于处理生产者和消费者模型，并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t，这样就可以定义一个条件变量类型的变量了：
    // pthread_cond_t cond;

    // 被条件变量阻塞的线程的线程信息会被记录到这个变量中，以便在解除阻塞的时候使用。
    // 条件变量操作函数函数原型如下：
    // 初始化操作, 其参数:
    //  * cond: 条件变量的地址
    //  * attr: 条件变量属性, 一般使用默认属性, 指定为NULL
    // int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    // 销毁释放资源        
    // int pthread_cond_destroy(pthread_cond_t *cond);

    // 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
    // 通过函数原型可以看出，该函数在阻塞线程的时候，需要一个互斥锁参数，这个互斥锁主要功能是进行线程同步，让线程顺序进入临界区，避免出现数共享资源的数据混乱。
    // 该函数会对这个互斥锁做以下几件事情：
    //  * 在阻塞线程时候，如果线程已经对互斥锁 mutex 上锁，那么会将这把锁打开，这样做是为了避免死锁
    //  * 当线程解除阻塞的时候，函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上，继续向下访问临界区
    // int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

    // 表示的时间是从 1971.1.1 到某个时间点的时间, 总长度使用秒/纳秒表示
    // struct timespec {
    //      time_t tv_sec;      /* Seconds */
    //      long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
    // };
    // 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
    //  * 这个函数的前两个参数和 pthread_cond_wait 函数是一样的，
    //  * 第三个参数表示线程阻塞的时长。
    // int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
    // 需要额外注意一点：struct timespec 这个结构体中记录的时间是从 1971.1.1 到某个时间点的时间，总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点：
    // time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
    // struct timespec tmsp;
    // tmsp.tv_nsec = 0;
    // tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s

    // 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
    // int pthread_cond_signal(pthread_cond_t *cond);
    // 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
    // int pthread_cond_broadcast(pthread_cond_t *cond);
    // 调用上面两个函数中的任意一个，都可以换线被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程，区别就在于：
    //  * pthread_cond_signal 是唤醒至少一个被阻塞的线程（总个数不定）；具体唤醒哪一个线程是由操作系统的调度策略和线程的优先级决定的，开发者无法直接控制。
    //                        在某些情况下，它可能会唤醒多个线程。这是因为线程调度和唤醒的具体实现是由操作系统控制的，不同的操作系统或者不同的配置可能会有不同的行为。
    //  * pthread_cond_broadcast 是唤醒所有被阻塞的线程。
    
    // summer：从上面这些操作和定义来看，条件变量提供的功能和 java 的并发操作差不多，提供了一种栅栏的解决方案。只是没有 java 那么封装的简单。 

    // 通过条件变量来实现生产者消费者模型
    // 生产者和消费者模型的组成：
    //  1. 生产者线程 -> 若干个
    //     * 生产商品或者任务放入到任务队列中
    //     * 任务队列满了就阻塞, 不满的时候就工作
    //     * 通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
    //  2. 消费者线程 -> 若干个
    //     * 读任务队列, 将任务或者数据取出
    //     * 任务队列中有数据就消费，没有数据就阻塞
    //     * 通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
    //  3. 队列 -> 存储任务/数据，对应一块内存，为了读写访问可以通过一个数据结构维护这块内存
    //     * 可以是数组、链表，也可以使用 stl 容器：queue / stack / list / vector
    /*
                            ┌───────┐
              ▪             │ Task  │             ▪
              ▪             │ queue │             ▪
              ▪             │       │             ▪
        ┌───────────┐       ├───────┤       ┌───────────┐
        │ producer3 │ ----> │       │ <---> │ consumer3 │
        └───────────┘       │       │       └───────────┘
        ┌───────────┐       ├───────┤       ┌───────────┐
        │ producer2 │ ----> │       │ <---> │ consumer2 │
        └───────────┘       │       │       └───────────┘
        ┌───────────┐       ├───────┤       ┌───────────┐
        │ producer1 │ ----> │       │ <---> │ consumer1 │
        └───────────┘       └───────┘       └───────────┘
    */
    // 场景描述：使用条件变量实现生产者和消费者模型，生产者有 5 个，往链表头部添加节点，消费者也有 5 个，删除链表头部的节点。
    /*
    void producerConsumerOfCondDemo();
    producerConsumerOfCondDemo();
    */


    // 2.4 信号量
    // 信号量用在多线程多任务同步的，一个线程完成了某一个动作就通过信号量告诉别的线程，别的线程再进行某些动作。
    // 信号量不一定是锁定某一个资源，而是流程上的概念，比如：有 A，B 两个线程，B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤，
    // 这个任务并不一定是锁定某一资源，还可以是进行一些计算或者数据处理之类。
    // 
    // '信号量（信号灯）' 与互斥锁和条件变量的主要不同在于”灯”的概念，灯亮则意味着资源可用，灯灭则意味着不可用。
    // 信号量主要阻塞线程, 不能完全保证线程安全，如果要保证线程安全, 需要 '信号量' 和 '互斥锁' 一起使用。
    //
    // 信号量和条件变量一样用于处理生产者和消费者模型，用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t 对应的头文件为 <semaphore.h>：
    // #include <semaphore.h>
    // sem_t sem;

    // Linux提供的信号量操作函数原型如下：

    // 初始化信号量/信号灯，其参数如下:
    //  * sem：信号量变量地址
    //  * pshared：
    //    - 0：  线程同步
    //    - 非0：进程同步
    //  * value：初始化当前信号量拥有的资源数（>=0），如果资源数为 0，线程就会被阻塞了。
    // int sem_init(sem_t *sem, int pshared, unsigned int value);
    // 资源释放, 线程销毁之后调用这个函数即可，其参数如下:
    //  * sem：就是 sem_init() 的第一个参数
    // int sem_destroy(sem_t *sem);

    // 当线程调用这个函数，并且 sem 中的资源数 >0，线程不会阻塞，线程会占用 sem 中的一个资源，因此资源数 -1，直到 sem 中的资源数减为 0 时，
    // 资源被耗尽，因此线程也就被阻塞了。
    // 
    // 函数被调用 sem 中的资源就会被消耗 1 个, 资源数 -1，其参数如下:
    //  * sem：就是 sem_init() 的第一个参数  
    // int sem_wait(sem_t *sem);

    // 当线程调用这个函数，并且 sem 中的资源数 >0，线程不会阻塞，线程会占用 sem 中的一个资源，因此资源数 -1，直到 sem 中的资源数减为 0 时，
    // 资源被耗尽，但是线程不会被阻塞，直接返回错误号，因此可以在程序中添加判断分支，用于处理获取资源失败之后的情况。
    // 
    // 函数被调用 sem 中的资源就会被消耗 1 个, 资源数 -1，其参数如下:
    //  * sem：就是 sem_init() 的第一个参数  
    // int sem_trywait(sem_t *sem);

    // 表示的时间是从 1971.1.1 到某个时间点的时间, 总长度使用秒/纳秒表示
    // struct timespec {
    //     time_t tv_sec;      /* Seconds */
    //     long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
    // };
    // 当线程调用这个函数，并且 sem 中的资源数 >0，线程不会阻塞，线程会占用 sem 中的一个资源，因此资源数 -1，直到 sem 中的资源数减为 0 时，
    // 资源被耗尽，线程被阻塞， 当阻塞指定的时长之后，线程解除阻塞。
    // 
    // 调用该函数线程获取 sem 中的一个资源，当资源数为 0 时，线程阻塞，在阻塞 abs_timeout 对应的时长之后，解除阻塞。
    //  * abs_timeout: 阻塞的时间长度, 单位是 s, 是从 1970.1.1 开始计算的
    // int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    // 该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的，使用方法不再过多赘述。

    // 调用该函数会将 sem 中的资源数 +1，如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了，这时这些线程会解除阻塞，获取到资源之后继续向下运行。
    // 调用该函数给 sem 中的资源数 +1
    // int sem_post(sem_t *sem);

    // 通过这个函数可以查看 sem 中现在拥有的资源个数，通过第二个参数 sval 将数据传出，也就是说第二个参数的作用和返回值是一样的。
    // 查看信号量 sem 中的整形数的当前值, 这个值会被写入到 sval 指针对应的内存中
    //  * sval 是一个传出参数
    // int sem_getvalue(sem_t *sem, int *sval);

    // 信号量使用
    //  - 场景描述：使用信号量实现生产者和消费者模型，生产者有5个，往链表头部添加节点，消费者也有5个，删除链表头部的节点。
    //  - 通过信号量来实现生产者消费者模型
    void producerConsumerOfSemDemo();
    producerConsumerOfSemDemo();
    // 通过上面的代码可以知道，初始化信号量的时候没有消费者分配资源，消费者线程启动之后由于没有资源自然就被阻塞了，
    // 等生产者生产出产品之后，再给消费者分配资源，这样二者就可以配合着完成生产和消费流程了。

}

// 链表的节点
struct Node {
    int number;
    Node* next;
};
// 指向头节点的指针
Node* headNode = nullptr;

// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t condMutex;

// 基于 cond 的生产者消费者演示
void producerConsumerOfCondDemo() {
    void* producerOfCond(void*);
    void* consumerOfCond(void*);

    pthread_cond_init(&cond, nullptr);
    pthread_mutex_init(&condMutex, nullptr);

    pthread_t producerTid[3];
    pthread_t consumerTid[3];
    for(int index = 0; index < 3; ++index) 
    {
        pthread_create(&producerTid[index], nullptr, producerOfCond, nullptr);
    }

    for(int index = 0; index < 3; ++index) 
    {
        pthread_create(&consumerTid[index], nullptr, consumerOfCond, nullptr);
    }

    for(int index = 0; index < 3; ++index) 
    {
        pthread_join(producerTid[index], nullptr);
        pthread_join(consumerTid[index], nullptr);
    }

    pthread_mutex_destroy(&condMutex);
    pthread_cond_destroy(&cond);
}
// 基于 cond 条件变量的生产者
void* producerOfCond(void*) {
    // 一直生产
    while(true)
    {
        pthread_mutex_lock(&condMutex);
        // 创建一个新节点
        struct Node* pNewNode = (struct Node*) malloc(sizeof(struct Node));
        // 初始化节点
        pNewNode->number = rand() % 1000;
        // 将新节点插入到队列节点头部只需要两步：1. 将新节点的 next 指向旧的节点头。2. 将新节点地址赋值给指向节点头的指针。
        pNewNode->next = headNode;
        headNode = pNewNode;
        cout << "生产者：tid::" + to_string(pthread_self()) + "; number = " + to_string(pNewNode->number) + "\n";
        pthread_mutex_unlock(&condMutex);

        // 通知消费者
        // pthread_cond_signal(&cond);
        pthread_cond_broadcast(&cond);

        sleep(rand() % 3);
    }
    return NULL;
}
// 基于 cond 条件变量的消费者
void* consumerOfCond(void*) {
    while(true) 
    {
        // 这里会对 condMutex 加锁，上锁后其他线程就无法获得锁了。
        // 注意生产者也使用同意把锁，如果消费者先进入，意味着生产者是没有办法获得锁的会阻塞，那就死锁了，但是实际上并没有这样。原因在下面代码
        pthread_mutex_lock(&condMutex);
        // 使用if 有bug:
        // 当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);
        // 当生产者生产了 1 个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程
        // - 有一个消费者线程通过 pthread_cond_wait() 加锁成功, 其余没有加锁成功的线程继续阻塞
        // - 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁
        // - 没有加锁成功的线程解除阻塞继续抢这把锁, 另外一个子线程加锁成功
        // - 但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误
        // 解决方案:
        // - 需要循环的对链表是否为空进行判断, 需要将 if 该成 while
        // summer补充:
        // - 当然我们也可以不使用 pthread_cond_broadcast 而使用 pthread_cond_signal，每次释放一个，但这样虽然看起来正常，但是并不安全，因为在不同系统上可能表现不一样。
        //   前面说过 pthread_cond_signal 是至少释放一个，在某些系统或机制下可能释放多个。其效果也得不到保障会遇到 pthread_cond_broadcast 一样的问题。
        // - 在多线程开发中所有涉及判断条件的地方，都应该优先考虑 while 来判断。          
        // if(headNode == nullptr) {
        while(headNode == nullptr) {
            // 这里调用条件变量等待的时候，不光需要传入条件变量，还需要传入一个互斥锁，其意义就是当条件变量进入等待时会做这么几件事：
            //  1. 将现在执行的线程放入等待队列。
            //  2. 放入等待队列后线程挂起了，其实就不会再继续执行了，此时将互斥锁释放。condMutex 又可以被其他线程争夺了。
            //  3. 当其他线程执行完毕后调用 pthread_cond_signal 或者 pthread_cond_broadcast 通知解锁时，等待队列的线程又会重新进入锁的争夺。
            //     获得锁的线程从等待队列出来，并且对互斥锁进行加锁操作，然后继续执行后续的代码。所以即便在 pthread_cond_wait 解锁后，
            //     我们依然需要自己主动调用 pthread_mutex_unlock 解锁。否则线程就会进入死锁状态。
            //     
            // 在 pthread_cond_wait 之前为什么要先加锁，是为了在进行条件判断之前，锁住条件，这样就不会因为在 pthread_cond_wait 之前共享数据被修改了导致数据混乱的问题。
            //  - 如：headNode == nullptr 刚刚判断完不为空，准备去执行时，其他线程拿走了 headNode 里唯一节点，导致线程执行下去没有节点可用。
            //  - 如：pthread_cond_wait 调用时一些方法内资源使用等问题。
            //
            // 由于 pthread_cond_wait 会自动释放互斥锁，所以在上面虽然有加锁操作，但是这里会释放掉锁，
            // 这样就不会因为在 wait 之前加锁后 wait 挂起而导致其他线程无锁可用的情况，其他线程又可以进入锁的争夺。
            pthread_cond_wait(&cond, &condMutex);
        }

        // 取出并删除节点头也分两步：1. 将 headNode 指向的指针保存。 2. 将 headNode 的指针指向它的下一个节点。
        struct Node* pNode = headNode;
        headNode = pNode->next;

        cout << "消费者：tid::" + to_string(pthread_self()) + "; number = " + to_string(pNode->number) + "\n";

        // 使用完了回收指针
        free(pNode);
        // delete pNode;

        pthread_mutex_unlock(&condMutex);

        sleep(rand() % 3);
    }
    return nullptr;
}


#include <semaphore.h>
// 生产者线程信号
sem_t psem;
// 消费者线程信号
sem_t csem;
// 互斥锁变量：使用信号量并不能真正的让线程安全，只能控制某些线程顺序。所以还需要互斥锁来保护线程数据安全
// 假如有一个大小为 3 的型号量，在 5 个线程里使用，这虽然会控制只有 3 个线程能够得到执行另外两个需要等待，但是以允许的 3 个线程对同一个变量的访问仍然是并行的。下面的消费者例子很好说明。
pthread_mutex_t semMutex;

// 基于 sem 信号量的生产者消费者演示
void producerConsumerOfSemDemo() {
    void* producerOfSem(void*);
    void* consumerOfSem(void*);

    // 初始化信号量
    //  * 总资源数为 1：
    //    - 如果生产者和消费者线程使用的信号量对应的总资源数为 1，那么不管线程有多少个，可以工作的线程只有一个，其余线程由于拿不到资源，都被迫阻塞了。并不会对某个资源并行访问，可以不加锁。
    //    - 通过测试可以得到如下结论：如果生产者和消费者使用的信号量总资源数为 1，那么不会出现生产者线程和消费者线程同时访问共享资源的情况，不管生产者和消费者线程有多少个，它们都是顺序执行的。
    //  * 总资源数大于 1：
    //    - 如果生产者和消费者线程使用的信号量对应的总资源数为大于 1，这种场景下出现的情况就比较多了：
    //      1. 多个生产者线程同时生产；
    //      2. 多个消费者同时消费；
    //      3. 生产者线程和消费者线程同时生产和消费。
    //    - 以上不管哪一种情况都可能会出现多个线程访问共享资源的情况，如果想防止共享资源出现数据混乱，那么就需要使用互斥锁进行线程同步：
    sem_init(&psem, 0, 1);  // 3 个生产者可以同时生产
    sem_init(&csem, 0, 0);  // 消费者线程没有资源, 因此不能消费

    // 如果信号量大于 1，那么说明会有几条并发执行流程线可以执行，此时需要对数据访问做加锁操作避免混乱
    pthread_mutex_init(&semMutex, nullptr);

    pthread_t producerTid[3];
    pthread_t consumerTid[3];
    for(int index = 0; index < 3; ++index) 
    {
        pthread_create(&producerTid[index], nullptr, producerOfSem, nullptr);
    }

    for(int index = 0; index < 3; ++index) 
    {
        pthread_create(&consumerTid[index], nullptr, consumerOfSem, nullptr);
    }

    for(int index = 0; index < 3; ++index) 
    {
        pthread_join(producerTid[index], nullptr);
        pthread_join(consumerTid[index], nullptr);
    }

    pthread_mutex_destroy(&semMutex);
    sem_destroy(&psem);
    sem_destroy(&csem);
}
// 基于 sem 信号量的生产者
void* producerOfSem(void*) {
    // 一直生产
    while(true)
    {
        // 在生产之前, 从信号量中取出一个资源
        sem_wait(&psem);
        // 加锁, 这句代码放到 sem_wait() 上边, 有可能会造成死锁
        pthread_mutex_lock(&semMutex);
        // 在上面的代码中，初始化状态下消费者线程没有任务信号量资源，假设某一个消费者线程先运行，调用 pthread_mutex_lock(&mutex);
        // 对互斥锁加锁成功，然后调用 sem_wait(&csem); 由于没有资源，因此被阻塞了。其余的消费者线程由于没有抢到互斥锁，因此被阻塞在互斥锁上。
        // 对应生产者线程第一步操作也是调用 pthread_mutex_lock(&mutex);，但是这时候互斥锁已经被消费者线程锁上了，所有生产者都被阻塞，到此为止，多余的线程都被阻塞了，程序产生了死锁。
        // 所以生产者和消费者都应该注意数据临界区的范围。


        // 创建一个新节点
        struct Node* pNewNode = (struct Node*) malloc(sizeof(struct Node));
        // 初始化节点
        pNewNode->number = rand() % 1000;
        // 将新节点插入到队列节点头部只需要两步：1. 将新节点的 next 指向旧的节点头。2. 将新节点地址赋值给指向节点头的指针。
        pNewNode->next = headNode;
        headNode = pNewNode;
        cout << "生产者：tid::" + to_string(pthread_self()) + "; number = " + to_string(pNewNode->number) + "\n";

        // 解锁，这句代码放在 sem_post() 下面，也可能会造成死锁。
        pthread_mutex_unlock(&semMutex);

        // 通知消费者消费，传递一个型号给消费者，让消费者解除阻塞，给消费者信号量添加资源。
        sem_post(&csem);

        sleep(rand() % 3);
    }
    return NULL;
}
// 基于 sem 信号量的消费者
void* consumerOfSem(void*) {
    while(true) 
    {
        // 消费者需要等待生产, 由于初始化时数量为0，默认启动之后应该阻塞。需要等待生产者生产完成后给消费者增加信号量
        sem_wait(&csem);
        // 加锁, 这句代码放到 sem_wait() 上边, 有可能会造成死锁
        pthread_mutex_lock(&semMutex);

        // 取出并删除节点头也分两步：1. 将 headNode 指向的指针保存。 2. 将 headNode 的指针指向它的下一个节点。
        struct Node* pNode = headNode;
        headNode = pNode->next;

        cout << "消费者：tid::" + to_string(pthread_self()) + "; number = " + to_string(pNode->number) + "\n";

        // 使用完了回收指针
        free(pNode);
        // delete pNode;

        // 解锁，这句代码放在 sem_post() 下面，也可能会造成死锁。
        pthread_mutex_unlock(&semMutex);

        // 消费完成, 通知生产者生产，将信号量归还给生产者，生产者信号量增加
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return nullptr;
}
